package com.gitee.wsl.doc.prosemirror.transform.map

// A mapping represents a pipeline of zero or more [step maps](#transform.StepMap). It has special
// provisions for losslessly handling mapping positions through a series of steps in which some
// steps are inverted versions of earlier steps. (This comes up
// when‘[rebasing](/docs/guide/#transform.rebasing)’ steps for collaboration or history management.)
class Mapping(
    // Create a new mapping with the given position maps. The step maps in this mapping.
    maps: List<StepMap> = emptyList(),
    internal var mirror: MutableList<Int>? = null,
    // The starting position in the `maps` array, used when `map` or `mapResult` is called.
    var from: Int = 0,
    // The end position in the `maps` array.
    var to: Int = maps.size
) : Mappable {
    private val stepMaps = maps.toMutableList()
    val maps: List<StepMap>
        get() = stepMaps

    // Create a mapping that maps only through a part of this one.
    fun slice(from: Int = 0, to: Int = this.stepMaps.size) = Mapping(this.stepMaps, this.mirror, from, to)

    internal fun copy() = Mapping(stepMaps.toMutableList(), this.mirror?.toMutableList(), this.from, this.to)

    // Add a step map to the end of this mapping. If `mirrors` is given, it should be the index of
    // the step map that is the mirror image of this one.
    fun appendMap(map: StepMap, mirrors: Int? = null) {
        this.stepMaps.add(map)
        this.to = this.stepMaps.size
        if (mirrors != null) this.setMirror(this.stepMaps.size - 1, mirrors)
    }

    // Add all the step maps in a given mapping to this one (preserving mirroring information).
    fun appendMapping(mapping: Mapping) {
        val startSize = stepMaps.size
        for (i in mapping.stepMaps.indices) {
            val mirr = mapping.getMirror(i)
            this.appendMap(mapping.stepMaps[i], if (mirr != null && mirr < i) startSize + mirr else null)
        }
    }

    // Finds the offset of the step map that mirrors the map at the given offset, in this mapping
    // (as per the second argument to `appendMap`).
    fun getMirror(n: Int): Int? {
        return this.mirror?.let {
            it.forEachIndexed { i, item ->
                if (item == n) return it[i + (if (i % 2 != 0) -1 else 1)]
            }
            return null
        }
    }

    fun setMirror(n: Int, m: Int) {
        if (this.mirror == null) this.mirror = mutableListOf()
        this.mirror?.add(n)
        this.mirror?.add(m)
    }

    // Append the inverse of the given mapping to this one.
    fun appendMappingInverted(mapping: Mapping) {
        val totalSize = this.maps.size + mapping.maps.size
        for (i in mapping.maps.size - 1 downTo 0) {
            val mirr = mapping.getMirror(i)
            this.appendMap(
                mapping.maps[i].invert(),
                if (mirr != null && mirr > i) totalSize - mirr - 1 else null
            )
        }
    }

    // Create an inverted version of this mapping.
    fun invert(): Mapping {
        val inverse = Mapping()
        inverse.appendMappingInverted(this)
        return inverse
    }

    // Map a position through this mapping.
    override fun map(pos: Int, assoc: Int): Int {
        var pos = pos
        if (this.mirror != null) return this._map(pos, assoc, true) as Int
        for (i in from until to) {
            pos = this.stepMaps[i].map(pos, assoc)
        }
        return pos
    }

    // Map a position through this mapping, returning a mapping result.
    override fun mapResult(pos: Int, assoc: Int): MapResult {
        return this._map(pos, assoc, false) as MapResult
    }

    @Suppress("ktlint:standard:function-naming")
    internal fun _map(pos: Int, assoc: Int, simple: Boolean): Any {
        var delInfo = 0
        var pos = pos

        var i = this.from
        while (i < to) {
            val map = this.stepMaps[i]
            val result = map.mapResult(pos, assoc)
            if (result.recover != null) {
                val corr = this.getMirror(i)
                if (corr != null && corr > i && corr < this.to) {
                    i = corr
                    pos = this.stepMaps[corr].recover(result.recover)
                    i++
                    continue
                }
            }

            delInfo = delInfo or result.delInfo
            pos = result.pos
            i++
        }

        return if (simple) pos else MapResult(pos, delInfo, null)
    }
}